/*
    PolledTimeout.h - Encapsulation of a polled Timeout

    Copyright (c) 2018 Daniel Salazar. All rights reserved.
    This file is part of the esp8266 core for Arduino environment.

    This library is free software; you can redistribute it and/or
    modify it under the terms of the GNU Lesser General Public
    License as published by the Free Software Foundation; either
    version 2.1 of the License, or (at your option) any later version.

    This library is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public
    License along with this library; if not, write to the Free Software
    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
*/

#pragma once

#include <limits>                  // std::numeric_limits
#include <type_traits>             // std::is_unsigned

#define IRAM_ATTR

namespace esp8266 {


namespace polledTimeout {

namespace YieldPolicy {

struct DoNothing {
    static void execute() {}
};

struct YieldOrSkip {
    static void execute() {} //{esp_yield();}
};

template <unsigned long delayMs>
struct YieldAndDelayMs {
    static void execute() {
        delay(delayMs);
    }
};

} //YieldPolicy

namespace TimePolicy {

struct TimeSourceMillis {
    // time policy in milli-seconds based on millis()

    using timeType = decltype(millis());
    static timeType time() {
        return millis();
    }
    static constexpr timeType ticksPerSecond    = 1000;
    static constexpr timeType ticksPerSecondMax = 1000;
};

struct TimeSourceCycles {
    // time policy based on esp_get_cycle_count()
    // this particular time measurement is intended to be called very often
    // (every loop, every yield)

    using timeType = decltype(rp2040.getCycleCount());
    static timeType time() {
        return rp2040.getCycleCount();
    }
    static constexpr timeType ticksPerSecond    = F_CPU;
    static constexpr timeType ticksPerSecondMax = F_CPU;
};

template <typename TimeSourceType, unsigned long long second_th>
// "second_th" units of timeType for one second
struct TimeUnit {
    using timeType = typename TimeSourceType::timeType;

#if __GNUC__ < 5
    // gcc-4.8 cannot compile the constexpr-only version of this function
    // using #defines instead luckily works
    static constexpr timeType computeRangeCompensation() {
#define number_of_secondTh_in_one_tick ((1.0 * second_th) / ticksPerSecond)
#define fractional (number_of_secondTh_in_one_tick - (long)number_of_secondTh_in_one_tick)

        return ({
            fractional == 0 ?
            1 : // no need for compensation
            (number_of_secondTh_in_one_tick / fractional) + 0.5; // scalar multiplier allowing exact division
        });

#undef number_of_secondTh_in_one_tick
#undef fractional
    }
#else
    static constexpr timeType computeRangeCompensation() {
        return ({
            constexpr double number_of_secondTh_in_one_tick = (1.0 * second_th) / ticksPerSecond;
            constexpr double fractional = number_of_secondTh_in_one_tick - (long)number_of_secondTh_in_one_tick;
            fractional == 0 ?
            1 : // no need for compensation
            (number_of_secondTh_in_one_tick / fractional) + 0.5; // scalar multiplier allowing exact division
        });
    }
#endif

    static constexpr timeType ticksPerSecond         = TimeSourceType::ticksPerSecond;
    static constexpr timeType ticksPerSecondMax      = TimeSourceType::ticksPerSecondMax;
    static constexpr timeType rangeCompensate        = computeRangeCompensation();
    static constexpr timeType user2UnitMultiplierMax = (ticksPerSecondMax * rangeCompensate) / second_th;
    static constexpr timeType user2UnitMultiplier    = (ticksPerSecond    * rangeCompensate) / second_th;
    static constexpr timeType user2UnitDivider       = rangeCompensate;
    // std::numeric_limits<timeType>::max() is reserved
    static constexpr timeType timeMax                = (std::numeric_limits<timeType>::max() - 1) / user2UnitMultiplierMax;

    static timeType toTimeTypeUnit(const timeType userUnit) {
        return (userUnit * user2UnitMultiplier) / user2UnitDivider;
    }
    static timeType toUserUnit(const timeType internalUnit) {
        return (internalUnit * user2UnitDivider) / user2UnitMultiplier;
    }
    static timeType time() {
        return TimeSourceType::time();
    }
};

using TimeMillis     = TimeUnit < TimeSourceMillis, 1'000 >;
using TimeFastMillis = TimeUnit < TimeSourceCycles, 1'000 >;
using TimeFastMicros = TimeUnit < TimeSourceCycles, 1'000'000 >;
using TimeFastNanos  = TimeUnit < TimeSourceCycles, 1'000'000'000 >;

} //TimePolicy

template <bool PeriodicT, typename YieldPolicyT = YieldPolicy::DoNothing, typename TimePolicyT = TimePolicy::TimeMillis>
class timeoutTemplate {
public:
    using timeType = typename TimePolicyT::timeType;
    static_assert(std::is_unsigned<timeType>::value == true, "timeType must be unsigned");

    static constexpr timeType alwaysExpired   = 0;
    static constexpr timeType neverExpires    = std::numeric_limits<timeType>::max();
    static constexpr timeType rangeCompensate = TimePolicyT::rangeCompensate; //debug

    timeoutTemplate(const timeType userTimeout) {
        reset(userTimeout);
    }

    IRAM_ATTR // fast
    bool expired() {
        YieldPolicyT::execute(); //in case of DoNothing: gets optimized away
        if (PeriodicT) {        //in case of false: gets optimized away
            return expiredRetrigger();
        }
        return expiredOneShot();
    }

    IRAM_ATTR // fast
    operator bool() {
        return expired();
    }

    bool canExpire() const {
        return !_neverExpires;
    }

    bool canWait() const {
        return _timeout != alwaysExpired;
    }

    // Resets, will trigger after this new timeout.
    IRAM_ATTR // called from ISR
    void reset(const timeType newUserTimeout) {
        reset();
        _timeout = TimePolicyT::toTimeTypeUnit(newUserTimeout);
        _neverExpires = (newUserTimeout < 0) || (newUserTimeout > timeMax());
    }

    // Resets, will trigger after the timeout previously set.
    IRAM_ATTR // called from ISR
    void reset() {
        _start = TimePolicyT::time();
    }

    // Resets to just expired so that on next poll the check will immediately trigger for the user,
    // also change timeout (after next immediate trigger).
    IRAM_ATTR // called from ISR
    void resetAndSetExpired(const timeType newUserTimeout) {
        reset(newUserTimeout);
        _start -= _timeout;
    }

    // Resets to just expired so that on next poll the check will immediately trigger for the user.
    IRAM_ATTR // called from ISR
    void resetAndSetExpired() {
        reset();
        _start -= _timeout;
    }

    void resetToNeverExpires() {
        _timeout = alwaysExpired + 1; // because canWait() has precedence
        _neverExpires = true;
    }

    timeType getTimeout() const {
        return TimePolicyT::toUserUnit(_timeout);
    }

    static constexpr timeType timeMax() {
        return TimePolicyT::timeMax;
    }

private:

    IRAM_ATTR // fast
    bool checkExpired(const timeType internalUnit) const {
        // canWait() is not checked here
        // returns "can expire" and "time expired"
        return (!_neverExpires) && ((internalUnit - _start) >= _timeout);
    }

protected:

    IRAM_ATTR // fast
    bool expiredRetrigger() {
        if (!canWait()) {
            return true;
        }

        timeType current = TimePolicyT::time();
        if (checkExpired(current)) {
            unsigned long n = (current - _start) / _timeout; //how many _timeouts periods have elapsed, will usually be 1 (current - _start >= _timeout)
            _start += n  * _timeout;
            return true;
        }
        return false;
    }

    IRAM_ATTR // fast
    bool expiredOneShot() const {
        // returns "always expired" or "has expired"
        return !canWait() || checkExpired(TimePolicyT::time());
    }

    timeType _timeout;
    timeType _start;
    bool _neverExpires;
};

// legacy type names, deprecated (unit is milliseconds)

using oneShot = polledTimeout::timeoutTemplate<false> /*__attribute__((deprecated("use oneShotMs")))*/;
using periodic = polledTimeout::timeoutTemplate<true> /*__attribute__((deprecated("use periodicMs")))*/;

// standard versions (based on millis())
// timeMax() is 49.7 days ((2^32)-2 ms)

using oneShotMs = polledTimeout::timeoutTemplate<false>;
using periodicMs = polledTimeout::timeoutTemplate<true>;

// Time policy based on esp_get_cycle_count(), and intended to be called very often:
// "Fast" versions sacrifices time range for improved precision and reduced execution time (by 86%)
// (cpu cycles for ::expired(): 372 (millis()) vs 52 (esp_get_cycle_count()))
// timeMax() values:
// Ms: max is 26843       ms (26.8  s)
// Us: max is 26843545    us (26.8  s)
// Ns: max is  1073741823 ns ( 1.07 s)
// (time policy based on esp_get_cycle_count() is intended to be called very often)

using oneShotFastMs = polledTimeout::timeoutTemplate<false, YieldPolicy::DoNothing, TimePolicy::TimeFastMillis>;
using periodicFastMs = polledTimeout::timeoutTemplate<true, YieldPolicy::DoNothing, TimePolicy::TimeFastMillis>;
using oneShotFastUs = polledTimeout::timeoutTemplate<false, YieldPolicy::DoNothing, TimePolicy::TimeFastMicros>;
using periodicFastUs = polledTimeout::timeoutTemplate<true, YieldPolicy::DoNothing, TimePolicy::TimeFastMicros>;
using oneShotFastNs = polledTimeout::timeoutTemplate<false, YieldPolicy::DoNothing, TimePolicy::TimeFastNanos>;
using periodicFastNs = polledTimeout::timeoutTemplate<true, YieldPolicy::DoNothing, TimePolicy::TimeFastNanos>;

} //polledTimeout


/*  A 1-shot timeout that auto-yields when in CONT can be built as follows:
    using oneShotYieldMs = esp8266::polledTimeout::timeoutTemplate<false, esp8266::polledTimeout::YieldPolicy::YieldOrSkip>;

    Other policies can be implemented by the user, e.g.: simple yield that panics in SYS, and the polledTimeout types built as needed as shown above, without modifying this file.
*/

}//esp8266
